Skip to content

VCST-5468: Resolve IMediator from request scope in GraphQL builders and add IRequestScopedCache#76

Open
alexeyshibanov wants to merge 9 commits into
devfrom
feat/VCST-5468
Open

VCST-5468: Resolve IMediator from request scope in GraphQL builders and add IRequestScopedCache#76
alexeyshibanov wants to merge 9 commits into
devfrom
feat/VCST-5468

Conversation

@alexeyshibanov

@alexeyshibanov alexeyshibanov commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Problem

graphql-dotnet *Type classes and ISchemaBuilder implementations are singletons (the schema is built once). A ctor-injected IMediator is captured against the root service provider, so every handler it dispatches to resolves from the root scope:

  • The moment any handler behind such a mediator gains an AddScoped dependency, requests fail with Cannot resolve scoped service ... from root provider — surfacing as HTTP 200 with per-field errors (invisible to status-only smoke checks). With ValidateScopes off, the scoped service is silently promoted to an app-global singleton (cross-request state sharing).
  • Request-scoped services are effectively unusable behind mediator-dispatched handlers, blocking a whole class of per-request optimizations.

Changes

  • RequestBuilder.GetResponseAsync resolves the mediator via the new IResolveFieldContext.GetMediator() extension — per-request scope, null-guarded with a diagnostic InvalidOperationException when RequestServices is not populated.
  • New constructors without IMediator across the builder chain (RequestBuilder, CommandBuilder, QueryBuilder, SearchQueryBuilder, LocalizedSettingQueryBuilder, GetStoreQueryBuilder, SlugInfoQueryBuilder) and the graph types / schema builders that captured a mediator (CountryType, DynamicPropertyType, DynamicPropertyValueType, CoreSchema, DynamicPropertySchema). Old constructors are kept as [Obsolete] delegating overloads (DiagnosticId = "VC0015") — binary- and source-compatible; consumers migrate at their own pace (or add <NoWarn>VC0015</NoWarn> under TreatWarningsAsErrors).
  • New IRequestScopedCache (Scoped): GetOrAddAsync<T>(key, factory) — request-scoped memoization that dedups a load re-issued with identical arguments many times within one GraphQL request. Complements DataLoader: DataLoader dedups by node key, this cache dedups by the load's own argument key. Implementation stores the Task<T> itself (no value-type boxing, no per-call closure), single-flight via Lazy.
  • Architecture guard test: no non-obsolete constructor of an IGraphType / ISchemaBuilder implementation may take IMediator.
  • ResolveFieldContextExtensions converted to C# 14 extension members; SetCurrencies no longer re-enumerates its source.

Notes

  • DI constructor selection: MS.DI still picks the widest (obsolete) constructor when both are resolvable. That path is harmless — the mediator parameter is discarded (the backing fields are removed), and dispatch always resolves from context.RequestServices.
  • Execution-path audit: RequestServices carries a live DI scope on all HTTP paths (single, batched sequential/parallel, GraphiQL). Subscription per-event resolution under the default strategy does not (the subscribe-time scope is disposed right after subscribe) — no resolver touched by this change is reachable there; if an emitting subscription ever needs scoped services, register ScopedSubscriptionExecutionStrategy.
  • The first consumer of IRequestScopedCache lands in a follow-up (x-cart configuration-query dedup, VCST-5303): measured on a real storefront cart read path — ES searches per read 207 -> 5, k6 50-VU p95 6042 -> 152 ms.

🤖 Generated with Claude Code


Note

Medium Risk
Changes the core GraphQL-to-Mediator path and DI lifetimes across many types; behavior is intended to fix scoped-service bugs but any missed resolver or test without RequestServices could fail at runtime.

Overview
Fixes singleton GraphQL schema/types capturing IMediator from the root scope, which breaks or mis-scopes handler dependencies. Dispatch now uses IResolveFieldContext.GetMediator() (from RequestServices inside resolvers); RequestBuilder.GetResponseAsync and affected graph types/schema builders follow that pattern instead of a stored mediator.

Constructors on query/command builders and core graph types gain IAuthorizationService-only entry points; IMediator ctors remain as [Obsolete] (VC0015) delegating overloads for binary/source compatibility.

Adds IRequestScopedCache (scoped DI) with GetOrAddAsync and batch GetOrLoadByIdsAsync, plus an IEntity id extension—intended for per-request dedup of repeated loads (follow-up consumers). ResolveFieldContextExtensions is refactored to C# 14 extension members (including GetMediator).

Tests cover request-scoped mediator resolution, a no non-obsolete IMediator ctor guard on IGraphType/ISchemaBuilder, and broad RequestScopedCache behavior.

Reviewed by Cursor Bugbot for commit 6446604. Bugbot is set up for automated code reviews on this repo. Configure here.

Jira-link:
https://virtocommerce.atlassian.net/browse/VCST-5468
Artifact URL:
https://vc3prerelease.blob.core.windows.net/packages/VirtoCommerce.Xapi_3.1014.0-alpha.194-vcst-5468.zip

alexeyshibanov and others added 4 commits July 13, 2026 18:32
…nd add IRequestScopedCache

Graph types and schema builders are singletons; a ctor-injected IMediator is root-bound and
cannot reach Scoped handler dependencies. GetResponseAsync and all resolver closures now resolve
the mediator via context.GetMediator() (request scope, null-guarded InvalidOperationException).
Old ctors are kept as [Obsolete] (VC0015) delegating overloads - nothing breaks at compile time.

Adds IRequestScopedCache: Scoped GetOrAddAsync(key, factory) primitive deduplicating a load
re-issued with identical arguments within one GraphQL request (first consumer: x-cart, follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…veFieldContextExtensions

RequestScopedCache: store the Task<T> itself (no value-type boxing), static GetOrAdd lambda
(no per-call closure), no async state machine, null-guard on factory.
ResolveFieldContextExtensions: C# 14 extension member block; SetCurrencies no longer
re-enumerates the source; tolerate IValueObject implementors not derived from ValueObject.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tScopedCache

QueryArguments constructed explicitly (collection expression lowered through an empty array).
RequestScopedCache.GetOrAddAsync uses the GetOrAdd(key, valueFactory, factoryArgument) overload
with a static lambda - no closure allocation on the hit path.
S3267 (foreach+if vs Where) is deliberately left: the loop form avoids the Where iterator
allocation and keeps the skip reason at the use site; the quality gate is not affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…opedCache docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alexeyshibanov and others added 5 commits July 14, 2026 21:27
Per-id promise reservation (TCS) bounds concurrent loads to the distinct union of
missing ids; tuple-keyed entries can't alias by-key entries; null load result counts
as empty (platform GetOrLoadByIdsAsync semantics). IEntity sugar as an extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Method-per-phase split (guards / classify / reserve / load+publish / fault / collect);
behavior and allocations unchanged, unified try/catch over classification+dispatch kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename from GetOrAddAsync overload; ids as ICollection<string> (no lazy re-enumeration
in overrides); result as IDictionary<string, T> - created per call, callers may mutate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ICollection input keeps zero-copy Keys passing; IList result matches VC service
returns (Task<T> invariance otherwise forces an async wrapper at every call site)
and bans deferred re-enumeration in overrides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants